視覺化與程式

以中選會 2018 選舉資料為例

Pyradise,郭耀仁

關於我們

關於我

資料科學與推廣教育的愛好者,台大商研所畢,創辦 DataInPoint 致力為資料科學愛好者提供最棒的教學資源。任職過上海的外商新創團隊、台北的外商軟體公司與民營銀行,閒暇時喜歡長跑與乒乓球。

議程

  • 動機
  • 前情提要
  • 視覺化套件簡介
  • 簡易視覺化
  • folium 視覺化

動機

2018-11-24 之後有感

  • 同溫層崩潰
  • 動起來,覺得應該做些什麼
  • 感謝 AWS、讚嘆 AWS!

有效的社群訊息傳播不再是文字論述

  • 長輩圖
  • Noun-Project 簡報圖
  • 資料佐證的資訊圖表

我打算怎麼動起來:傳播技能

  1. 如何用程式幫助資料的擷取與整併
  2. 如何用程式製作視覺化
  3. 如何用 Adobe XD 或 Vectr 美化向量圖表

前情提要

投票所明細資料來源

中選會選舉資料庫網站

直轄市長、縣市長各投票所明細

台北市為例

直轄市議員、縣市議員各投票所明細

台北市為例

公投(各投開票所)得票數一覽表

台北市為例

預期產出

  • 直轄市長、縣市長各投票所明細整併為一個長表格
  • 直轄市議員、縣市議員各投票所明細整併為一個長表格
  • 公投各投票所明細整併為一個寬表格

直轄市長、縣市長資料整併

  • 判斷有幾個候選人、姓名與黨籍
  • 選定必要欄位
  • 給定欄位名
  • 填補行政區缺失
  • 刪除行政區得票數小計列
  • 轉置
  • 垂直合併
In [1]:
from tidy_election_data import get_tidy_df

city_ids = [100, 200, 300, 400, 500, 600]
county_ids = list(range(701, 715)) + [801, 802]
cities = ["台北市", "新北市", "桃園市", "台中市", "台南市", "高雄市"]
counties = ["新竹縣", "苗栗縣", "彰化縣", "南投縣", "雲林縣", "嘉義縣", "屏東縣", "宜蘭縣", "花蓮縣", "台東縣", "澎湖縣", "基隆市", "新竹市", "嘉義市", "金門縣", "連江縣"]
admin_areas = cities + counties
city_xls_file_urls = ["https://s3-ap-northeast-1.amazonaws.com/tw-election-2018/city-mayor/{}.xls".format(cid) for cid in city_ids]
county_xls_file_urls = ["https://s3-ap-northeast-1.amazonaws.com/tw-election-2018/county-mayor/{}.xls".format(cid) for cid in county_ids]
xls_file_urls = city_xls_file_urls + county_xls_file_urls
df_dict = dict()
for file_url, admin_area in zip(xls_file_urls, admin_areas):
  tidy_df = get_tidy_df(file_url)
  long_df = tidy_df[1]
  long_df.insert(0, "admin_area", admin_area)
  df_dict[admin_area] = long_df
候選人人數:5
候選人姓名:
1 無黨籍 吳蕚洋
2 中國國民黨 丁守中
3 民主進步黨 姚文智
4 無黨籍 柯文哲
5 無黨籍 李錫錕
資料框的列數為:1563,投票所個數為:1563
候選人人數:2
候選人姓名:
1 民主進步黨 蘇貞昌
2 中國國民黨 侯友宜
資料框的列數為:2446,投票所個數為:2446
候選人人數:5
候選人姓名:
1 無黨籍 朱梅雪
2 中國國民黨 陳學聖
3 無黨籍 楊麗環
4 無黨籍 吳富彤
5 民主進步黨 鄭文燦
資料框的列數為:1142,投票所個數為:1142
候選人人數:3
候選人姓名:
1 無黨籍 宋原通
2 民主進步黨 林佳龍
3 中國國民黨 盧秀燕
資料框的列數為:1601,投票所個數為:1601
候選人人數:6
候選人姓名:
1 民主進步黨 黃偉哲
2 中國國民黨 高思博
3 無黨籍 林義豐
4 無黨籍 許忠信
5 無黨籍 陳永和
6 無黨籍 蘇煥智
資料框的列數為:1322,投票所個數為:1322
候選人人數:4
候選人姓名:
1 中國國民黨 韓國瑜
2 民主進步黨 陳其邁
3 無黨籍 璩美鳳
4 無黨籍 蘇盈貴
資料框的列數為:1823,投票所個數為:1823
候選人人數:4
候選人姓名:
1 中國國民黨 楊文科
2 無黨籍 葉芳棟
3 民主進步黨 鄭朝方
4 民國黨 徐欣瑩
資料框的列數為:404,投票所個數為:404
候選人人數:4
候選人姓名:
1 無黨籍 朱泰平
2 無黨籍 徐定禎
3 無黨籍 黃玉燕
4 中國國民黨 徐耀昌
資料框的列數為:470,投票所個數為:470
候選人人數:5
候選人姓名:
1 民主進步黨 魏明谷
2 中國國民黨 王惠美
3 無黨籍 白雅燦
4 無黨籍 黃文玲
5 無黨籍 洪敏雄
資料框的列數為:1049,投票所個數為:1049
候選人人數:2
候選人姓名:
1 中國國民黨 林明溱
2 民主進步黨 洪國浩
資料框的列數為:488,投票所個數為:488
候選人人數:4
候選人姓名:
1 無黨籍 林佳瑜
2 民主進步黨 李進勇
3 無黨籍 王麗萍
4 中國國民黨 張麗善
資料框的列數為:557,投票所個數為:557
候選人人數:4
候選人姓名:
1 民主進步黨 翁章梁
2 無黨籍 吳芳銘
3 無黨籍 林國龍
4 中國國民黨 吳育仁
資料框的列數為:486,投票所個數為:486
候選人人數:3
候選人姓名:
1 無黨籍 李鎔任
2 中國國民黨 蘇清泉
3 民主進步黨 潘孟安
資料框的列數為:689,投票所個數為:689
候選人人數:5
候選人姓名:
1 無黨籍 林信華
2 中國國民黨 林姿妙
3 民主進步黨 陳歐珀
4 無黨籍 林錦坤
5 無黨籍 陳秋境
資料框的列數為:399,投票所個數為:399
候選人人數:3
候選人姓名:
1 中國國民黨 徐榛蔚
2 民主進步黨 劉曉玫
3 無黨籍 黄師鵬
資料框的列數為:299,投票所個數為:299
候選人人數:5
候選人姓名:
1 無黨籍 鄺麗貞
2 民主進步黨 劉櫂豪
3 無黨籍 黃裕斌
4 無黨籍 彭權國
5 中國國民黨 饒慶鈴
資料框的列數為:213,投票所個數為:213
候選人人數:7
候選人姓名:
1 無黨籍 翁珍聖
2 無黨籍 鄭清發
3 無黨籍 陳大松
4 無黨籍 吳政隆
5 中國國民黨 賴峰偉
6 民主進步黨 陳光復
7 無黨籍 呂華苑
資料框的列數為:116,投票所個數為:116
候選人人數:2
候選人姓名:
1 中國國民黨 謝立功
2 民主進步黨 林右昌
資料框的列數為:257,投票所個數為:257
候選人人數:6
候選人姓名:
1 無黨籍 謝文進
2 無黨籍 李驥羣
3 無黨籍 黃源甫
4 中國國民黨 許明財
5 無黨籍 郭榮睿
6 民主進步黨 林智堅
資料框的列數為:299,投票所個數為:299
候選人人數:4
候選人姓名:
1 無黨籍 蕭淑麗
2 中國國民黨 黃敏惠
3 無黨籍 黃宏成台灣阿成世界偉人財神總統
4 民主進步黨 涂醒哲
資料框的列數為:177,投票所個數為:177
候選人人數:6
候選人姓名:
1 中國國民黨 楊鎮浯
2 金門高粱黨 洪志恒
3 教科文預算保障e聯盟 汪承樺
4 無黨籍 陳福海
5 無黨籍 謝宜璋
6 無黨籍 洪和成
資料框的列數為:78,投票所個數為:78
候選人人數:4
候選人姓名:
1 樹黨 蘇柏豪
2 無黨籍 朱秀珍
3 無黨籍 魏耀乾
4 中國國民黨 劉增應
資料框的列數為:9,投票所個數為:9
In [2]:
import pandas as pd

mayors = pd.DataFrame()
for k in df_dict:
  mayors = mayors.append(df_dict[k], ignore_index=True)
print(mayors.shape)
mayors.head()
mayors.to_csv("mayors.csv", index=False)
(62689, 8)

從 Colaboratory 上將檔案下載到本機

from google.colab import files

!ls
files.download("mayors.csv")

直接用 pandas 讀入

In [3]:
import pandas as pd

mayors = pd.read_csv("https://s3-ap-northeast-1.amazonaws.com/tw-election-2018/mayors.csv")
print(mayors.shape)
(62689, 8)
In [4]:
mayors.head()
Out[4]:
admin_area district village office number party candidate votes
0 台北市 北投區 建民里 1 1 無黨籍 吳蕚洋 4
1 台北市 北投區 建民里 2 1 無黨籍 吳蕚洋 2
2 台北市 北投區 建民里 3 1 無黨籍 吳蕚洋 2
3 台北市 北投區 文林里 4 1 無黨籍 吳蕚洋 1
4 台北市 北投區 文林里 5 1 無黨籍 吳蕚洋 5
In [5]:
mayors.tail()
Out[5]:
admin_area district village office number party candidate votes
62684 連江縣 北竿鄉 后沃村、橋仔村、塘岐村 5 4 中國國民黨 劉增應 838
62685 連江縣 北竿鄉 坂里村、白沙村、芹壁村 6 4 中國國民黨 劉增應 301
62686 連江縣 莒光鄉 田沃村、西坵村、青帆村 7 4 中國國民黨 劉增應 341
62687 連江縣 莒光鄉 大坪村、福正村 8 4 中國國民黨 劉增應 391
62688 連江縣 東引鄉 樂華村、中柳村 9 4 中國國民黨 劉增應 396
In [6]:
mayors.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 62689 entries, 0 to 62688
Data columns (total 8 columns):
admin_area    62689 non-null object
district      62689 non-null object
village       62689 non-null object
office        62689 non-null int64
number        62689 non-null int64
party         62689 non-null object
candidate     62689 non-null object
votes         62689 non-null int64
dtypes: int64(3), object(5)
memory usage: 3.8+ MB

視覺化套件簡介

Python 的常用視覺化套件

  • 靜態
    • matplotlib.pyplot
    • pandas
    • seaborn
  • 互動(htmlWidget)
    • bokeh
    • plotly / dash
    • folium

matplotlib.pyplot

pandas

seaborn

  • 以 matplotlib 為基礎包裝好的高階統計視覺化套件
  • 以統計分析目的作為大分類
  • 作圖的資料單位是 DataFrame
  • https://seaborn.pydata.org/

bokehplotlydash

folium

簡易視覺化

matplotlib.pyplot

In [7]:
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties

myfont = FontProperties(fname="/System/Library/Fonts/STHeiti Light.ttc")
tp = mayors[mayors["admin_area"] == "台北市"]
grouped = tp.groupby(["number", "candidate"])
ttl_votes = pd.DataFrame(grouped["votes"].sum()).reset_index()
ttl_votes
Out[7]:
number candidate votes
0 1 吳蕚洋 5617
1 2 丁守中 577566
2 3 姚文智 244641
3 4 柯文哲 580820
4 5 李錫錕 6172
In [8]:
plt.barh(y=ttl_votes["number"].values, width=ttl_votes["votes"].values)
plt.yticks(ttl_votes["number"].values, ttl_votes["candidate"].values, fontproperties=myfont)
plt.title("台北市長得票數", fontproperties=myfont)
plt.show()

pandas

In [9]:
import pandas as pd

tp = mayors[mayors["admin_area"] == "台北市"]
grouped = tp.groupby(["number", "candidate"])
ttl_votes = pd.DataFrame(grouped["votes"].sum()).reset_index()
ttl_votes
Out[9]:
number candidate votes
0 1 吳蕚洋 5617
1 2 丁守中 577566
2 3 姚文智 244641
3 4 柯文哲 580820
4 5 李錫錕 6172
In [10]:
ax = ttl_votes.plot(x="candidate", y="votes", kind="barh")
ax.set_yticklabels(ttl_votes['candidate'], fontproperties=myfont)
plt.show()

seaborn

In [11]:
import pandas as pd
import seaborn as sns

tp = mayors[mayors["admin_area"] == "台北市"]
grouped = tp.groupby(["number", "candidate"])
ttl_votes = pd.DataFrame(grouped["votes"].sum()).reset_index()
ttl_votes
Out[11]:
number candidate votes
0 1 吳蕚洋 5617
1 2 丁守中 577566
2 3 姚文智 244641
3 4 柯文哲 580820
4 5 李錫錕 6172
In [12]:
sns.barplot(x="votes", y="number", data=ttl_votes, orient="h", ci=None)
plt.show()
In [13]:
import pandas as pd
import seaborn as sns

city_names = {
  "台北市": "Taipei",
  "新北市": "New Taipei",
  "桃園市": "Taoyuan",
  "台中市": "Taichung",
  "台南市": "Tainan",
  "高雄市": "Kaohsiung"
}
cities = mayors[mayors["admin_area"].isin(["台北市", "新北市", "桃園市", "台中市", "台南市", "高雄市"])]
grouped = cities.groupby(["admin_area", "number"])
ttl_votes = pd.DataFrame(grouped["votes"].sum()).reset_index()
ttl_votes["admin_area_en"] = ttl_votes["admin_area"].map(city_names)
ttl_votes
Out[13]:
admin_area number votes admin_area_en
0 台中市 1 15919 Taichung
1 台中市 2 619855 Taichung
2 台中市 3 827996 Taichung
3 台北市 1 5617 Taipei
4 台北市 2 577566 Taipei
5 台北市 3 244641 Taipei
6 台北市 4 580820 Taipei
7 台北市 5 6172 Taipei
8 台南市 1 367518 Tainan
9 台南市 2 312874 Tainan
10 台南市 3 84153 Tainan
11 台南市 4 45168 Tainan
12 台南市 5 117179 Tainan
13 台南市 6 39778 Tainan
14 新北市 1 873692 New Taipei
15 新北市 2 1165130 New Taipei
16 桃園市 1 18200 Taoyuan
17 桃園市 2 407234 Taoyuan
18 桃園市 3 51518 Taoyuan
19 桃園市 4 3867 Taoyuan
20 桃園市 5 552330 Taoyuan
21 高雄市 1 892545 Kaohsiung
22 高雄市 2 742239 Kaohsiung
23 高雄市 3 7998 Kaohsiung
24 高雄市 4 14125 Kaohsiung
In [14]:
g = sns.FacetGrid(ttl_votes, col="admin_area_en", col_wrap=3)
In [15]:
g = sns.FacetGrid(ttl_votes, col="admin_area_en", col_wrap=3, sharey=False, sharex=False, col_order=["Taipei", "New Taipei", "Taoyuan", "Taichung", "Tainan", "Kaohsiung"])
g.map(sns.barplot, "votes", "number", orient="h", order=None)
plt.show()

folium 視覺化

安裝

  • 終端機
pip install --upgrade folium
#conda install folium -c conda-forge
  • Google Colaboratory
!pip install --upgrade folium

Quickstart

In [16]:
import folium

m = folium.Map(location=[25.0389, 121.5682], zoom_start=15)
In [17]:
m
Out[17]:

添加標記(marker)

In [18]:
import folium

marker_loc = [25.0389, 121.5682]
tooltip = 'Click!'
m = folium.Map(location=marker_loc, zoom_start=15)
folium.Marker(marker_loc, popup='台灣亞馬遜網路服務有限公司', tooltip=tooltip).add_to(m)
Out[18]:
<folium.map.Marker at 0x11a43e898>
In [19]:
m
Out[19]:

添加邊界(margin)

GeoJSON 檔案

GeoJSON 來源:https://github.com/g0v/twgeojson

先暸解 GeoJSON 的內容

In [20]:
import requests

geojson = "https://s3-ap-northeast-1.amazonaws.com/tw-election-2018/twCounty2010.geo.json"
r = requests.get(geojson)
geojson_as_dict = r.json()
print(geojson_as_dict.keys())
dict_keys(['type', 'features'])
In [21]:
for i in range(len(geojson_as_dict["features"])):
  print(geojson_as_dict["features"][i]["properties"])
{'COUNTYSN': '10014001', 'COUNTYNAME': '台東縣', 'name': '台東縣'}
{'COUNTYSN': '10002001', 'COUNTYNAME': '宜蘭縣', 'name': '宜蘭縣'}
{'COUNTYSN': '63000001', 'COUNTYNAME': '台北市', 'name': '台北市'}
{'COUNTYSN': '10009001', 'COUNTYNAME': '雲林縣', 'name': '雲林縣'}
{'COUNTYSN': '10003001', 'COUNTYNAME': '桃園縣', 'name': '桃園縣'}
{'COUNTYSN': '10013001', 'COUNTYNAME': '屏東縣', 'name': '屏東縣'}
{'COUNTYSN': '10006001', 'COUNTYNAME': '台中市', 'name': '台中市'}
{'COUNTYSN': '10011001', 'COUNTYNAME': '台南市', 'name': '台南市'}
{'COUNTYSN': '10017001', 'COUNTYNAME': '基隆市', 'name': '基隆市'}
{'COUNTYSN': '09007001', 'COUNTYNAME': '連江縣', 'name': '連江縣'}
{'COUNTYSN': '10008001', 'COUNTYNAME': '南投縣', 'name': '南投縣'}
{'COUNTYSN': '10016001', 'COUNTYNAME': '澎湖縣', 'name': '澎湖縣'}
{'COUNTYSN': '10005001', 'COUNTYNAME': '苗栗縣', 'name': '苗栗縣'}
{'COUNTYSN': '10020001', 'COUNTYNAME': '嘉義市', 'name': '嘉義市'}
{'COUNTYSN': '10004001', 'COUNTYNAME': '新竹縣', 'name': '新竹縣'}
{'COUNTYSN': '10001001', 'COUNTYNAME': '新北市', 'name': '新北市'}
{'COUNTYSN': '10015001', 'COUNTYNAME': '花蓮縣', 'name': '花蓮縣'}
{'COUNTYSN': '10012001', 'COUNTYNAME': '高雄市', 'name': '高雄市'}
{'COUNTYSN': '10007001', 'COUNTYNAME': '彰化縣', 'name': '彰化縣'}
{'COUNTYSN': '10010001', 'COUNTYNAME': '嘉義縣', 'name': '嘉義縣'}
{'COUNTYSN': '09020001', 'COUNTYNAME': '金門縣', 'name': '金門縣'}
{'COUNTYSN': '10018001', 'COUNTYNAME': '新竹市', 'name': '新竹市'}
In [22]:
import folium

m = folium.Map(location=[24, 121], zoom_start=7, tiles='Mapbox Bright')
geojson = "twCounty2010.geo.json"
folium.GeoJson(
  geojson
).add_to(m)
Out[22]:
<folium.features.GeoJson at 0x11a22bda0>
In [23]:
m
Out[23]:

Choropleth Map

Geo/TopoJSON 來源:https://github.com/g0v/twgeojson

使用 google.colabfiles.upload() 上傳 GeoJSON

from google.colab import files

files.upload()
In [24]:
import pandas as pd
import folium

grouped = mayors.groupby(["admin_area", "party"])
ttl_votes_by_party = pd.DataFrame(grouped["votes"].sum()).reset_index()
ttl_votes_by_party[ttl_votes_by_party["party"] == "中國國民黨"]
Out[24]:
admin_area party votes
0 南投縣 中國國民黨 195385
2 台中市 中國國民黨 827996
5 台北市 中國國民黨 577566
8 台南市 中國國民黨 312874
11 台東縣 中國國民黨 70577
14 嘉義市 中國國民黨 58558
17 嘉義縣 中國國民黨 84243
20 基隆市 中國國民黨 86529
22 宜蘭縣 中國國民黨 123767
25 屏東縣 中國國民黨 197518
28 彰化縣 中國國民黨 377795
31 新北市 中國國民黨 1165130
33 新竹市 中國國民黨 60508
36 新竹縣 中國國民黨 107877
40 桃園市 中國國民黨 407234
43 澎湖縣 中國國民黨 20570
46 花蓮縣 中國國民黨 121297
49 苗栗縣 中國國民黨 175756
51 連江縣 中國國民黨 4861
54 金門縣 中國國民黨 23520
58 雲林縣 中國國民黨 210770
61 高雄市 中國國民黨 892545
In [25]:
grouped = mayors.groupby(["admin_area"])
ttl_votes_by_area = pd.DataFrame(grouped["votes"].sum()).reset_index()
ttl_votes_by_area
Out[25]:
admin_area votes
0 南投縣 292845
1 台中市 1463770
2 台北市 1414816
3 台南市 966670
4 台東縣 119518
5 嘉義市 142208
6 嘉義縣 285147
7 基隆市 188696
8 宜蘭縣 250121
9 屏東縣 470146
10 彰化縣 710419
11 新北市 2038822
12 新竹市 217103
13 新竹縣 282405
14 桃園市 1033149
15 澎湖縣 52915
16 花蓮縣 169596
17 苗栗縣 304370
18 連江縣 7408
19 金門縣 49229
20 雲林縣 391519
21 高雄市 1656907
In [26]:
kmt_votes_percentage = ttl_votes_by_party[ttl_votes_by_party["party"] == "中國國民黨"].reset_index(drop=True)
kmt_votes_percentage["ttl_votes"] = ttl_votes_by_area["votes"].values
kmt_votes_percentage["votes_percentage"] = kmt_votes_percentage["votes"] / kmt_votes_percentage["ttl_votes"]
kmt_votes_percentage
Out[26]:
admin_area party votes ttl_votes votes_percentage
0 南投縣 中國國民黨 195385 292845 0.667196
1 台中市 中國國民黨 827996 1463770 0.565660
2 台北市 中國國民黨 577566 1414816 0.408227
3 台南市 中國國民黨 312874 966670 0.323662
4 台東縣 中國國民黨 70577 119518 0.590514
5 嘉義市 中國國民黨 58558 142208 0.411777
6 嘉義縣 中國國民黨 84243 285147 0.295437
7 基隆市 中國國民黨 86529 188696 0.458563
8 宜蘭縣 中國國民黨 123767 250121 0.494829
9 屏東縣 中國國民黨 197518 470146 0.420121
10 彰化縣 中國國民黨 377795 710419 0.531792
11 新北市 中國國民黨 1165130 2038822 0.571472
12 新竹市 中國國民黨 60508 217103 0.278706
13 新竹縣 中國國民黨 107877 282405 0.381994
14 桃園市 中國國民黨 407234 1033149 0.394168
15 澎湖縣 中國國民黨 20570 52915 0.388737
16 花蓮縣 中國國民黨 121297 169596 0.715211
17 苗栗縣 中國國民黨 175756 304370 0.577442
18 連江縣 中國國民黨 4861 7408 0.656183
19 金門縣 中國國民黨 23520 49229 0.477767
20 雲林縣 中國國民黨 210770 391519 0.538339
21 高雄市 中國國民黨 892545 1656907 0.538681
In [27]:
import folium

m = folium.Map(location=[24, 121], zoom_start=7, tiles='Mapbox Bright')
twCounty = "twCounty2010.geo.json"
folium.Choropleth(
    geo_data=twCounty,
    name='choropleth',
    data=kmt_votes_percentage,
    columns=['admin_area', 'votes_percentage'],
    key_on='feature.properties.COUNTYNAME',
    fill_color='GnBu',
    fill_opacity=0.7,
    line_opacity=0.2,
    legend_name='votes ratio'
).add_to(m)

folium.LayerControl().add_to(m)
Out[27]:
<folium.map.LayerControl at 0x1186fe898>
In [28]:
m
Out[28]: